#!/usr/bin/env python

import sys
import re
import textwrap
import unittest


MAX_SUBJECT_LENGTH = 120
MAX_DESCRIPTION_LINE_LENGTH = 100


class TestProcessCommitMessage(unittest.TestCase):
    def test_msg_none(self):
        self.assertEqual(process_commit_message(None), None)

    def test_msg_empty(self):
        self.assertEqual(process_commit_message(""), "")

    def test_msg_whitespace_only(self):
        msg = "  \t "
        self.assertEqual(process_commit_message(msg), msg)

    def test_msg_newlines_only(self):
        msg = "\n" * 10
        self.assertEqual(process_commit_message(msg), msg)

    def test_msg_comments_only(self):
        msg = "\n# comment\n#another comment\n"
        self.assertEqual(process_commit_message(msg), msg)

    def test_msg_valid(self):
        msg = "Good subject line\n\nGood commit message"
        self.assertEqual(process_commit_message(msg), msg)

    def test_subject_valid(self):
        msg = "Good subject line"
        self.assertEqual(process_commit_message(msg), msg)

    def test_msg_extraneous_newlines(self):
        self.assertEqual(process_commit_message("\nGood subject line\n\n\nGood commit message"),
            "Good subject line\n\nGood commit message")

    def test_msg_subject_and_description_not_separated(self):
        self.assertEqual(process_commit_message("Good subject line\nGood commit message"),
            "Good subject line\n\nGood commit message")

    def test_subject_maxlen(self):
        msg = ("A" * MAX_SUBJECT_LENGTH) + "\n"
        self.assertEqual(process_commit_message(msg), msg[:-1])

    def test_subject_trailing_periods(self):
        self.assertEqual(process_commit_message("Subject line that ends with a period..\n\ndescription."),
            "Subject line that ends with a period\n\ndescription.")

    def test_subject_first_letter_lowercase(self):
        self.assertEqual(process_commit_message("subject line with a lowercase first letter\n\ndescription"),
            "Subject line with a lowercase first letter\n\ndescription")

    def test_description_unwrapped(self):
        result = process_commit_message("Good subject\n\n" + "a" * 200 + '\n' + 'b' * 200)
        self.assertTrue(all(len(line) <= MAX_DESCRIPTION_LINE_LENGTH for line in result.splitlines()))

    def test_subject_too_long(self):
        with self.assertRaises(ValueError):
            process_commit_message("A" * (MAX_SUBJECT_LENGTH + 1))

    def test_subject_periods_only(self):
        with self.assertRaises(ValueError):
            process_commit_message("...")

    def test_subject_jira_references(self):
        msgs = ["Good subject\n\nBad description DDSDBUS-1", "LISAMZA-123: Bad subject",
                "\nBad subject datapipes-12345\nGood description\n\n"]
        for msg in msgs:
            with self.assertRaises(ValueError):
                process_commit_message(msg)


class TerminalColors(object):
    RED = '\033[91m'
    YELLOW = '\033[93m'

    @staticmethod
    def format(s, color):
        return color + s + '\033[0m'

    @staticmethod
    def fail(s):
        return TerminalColors.format(s, TerminalColors.RED)

    @staticmethod
    def warn(s):
        return TerminalColors.format(s, TerminalColors.YELLOW)


def print_error(msg):
    error_msg = TerminalColors.fail('git commit failed -- {}\n') + \
                TerminalColors.warn('Git commit messages style guide can be accessed at:\n'
                     'https://github.com/linkedin/Brooklin/blob/master/CONTRIBUTING.md#git-commit-messages-style-guide')
    print(error_msg.format(msg))


def print_warning(msg):
    print TerminalColors.warn(msg)


def process_commit_message(msg):
    """
    Processes a Git commit message to ensure it meets the Git commit style guidelines documented at:
        https://github.com/linkedin/Brooklin/blob/master/CONTRIBUTING.md#git-commit-messages-style-guide

    Args:
        msg (str):  The entire Git commit message (possibly multiline)

    Returns:
        str: The processed message with minor improvements applied if applicable.
             These improvements are:
                - Capitalizing first letter of subject line
                - Removing periods from the end of subject line
                - Inserting a newline between subject line and description
                - Wrapping the description at 100 characters

    Raises:
        ValueError: if msg contains style violations that cannot be fixed automatically
                    These violations are:
                        1. A subject line whose length exceeds 120 characters
                        2. A reference to an internal Jira ticket (DDSDBUS, LISAMZA, or DATAPIPES)
    """
    if not msg:
        return msg

    # Filter out comments as well as empty lines added by previous Git hooks
    msg_lines = []
    for msg_line in msg.splitlines():
        msg_line = msg_line.strip()
        if msg_line and not msg_line.startswith('#'):
            msg_lines.append(msg_line)

    # If msg is composed entirely of new lines or comments, just return it as is.
    if not msg_lines:
        return msg

    subject = msg_lines[0].strip()

    # Limit the subject line to 120 characters
    if len(subject) > MAX_SUBJECT_LENGTH:
        raise ValueError("Commit message subject line is too long [{} chars max]".format(MAX_SUBJECT_LENGTH))

    # Do not make references to internal Jira tickets
    if re.search('(DDSDBUS|LISAMZA|DATAPIPES)-\d+', "".join(msg_lines), flags=re.IGNORECASE):
        raise ValueError("Commit messages must not reference internal Jira tickets (DDSDBUS, LISAMZA, DATAPIPES)")

    # Capitalize the subject line
    if subject[0].islower():
        subject = subject.capitalize()
        print_warning("Capitalized first letter of commit message subject line")

    # Do not end the subject line with a period
    if subject[-1] == '.':
        subject = subject.rstrip(".")
        if not subject:
            raise ValueError("Commit messages cannot have subject lines composed entirely of periods")
        print_warning("Removed trailing period(s) from commit message subject line")

    # Wrap the body at 100 characters
    description = []
    for desc_line in msg_lines[1:]:
        description.append("\n".join(textwrap.wrap(desc_line, MAX_DESCRIPTION_LINE_LENGTH)))
    if any(len(line) > MAX_DESCRIPTION_LINE_LENGTH for line in msg_lines):
        print_warning("Wrapping lines longer than {} chars in commit message description".format(MAX_DESCRIPTION_LINE_LENGTH))

    # Separate subject from body with a blank line
    return subject if not description else subject + "\n\n" + "\n".join(description)


def main():
    if len(sys.argv) == 2:
        commit_filename = sys.argv[1]
        try:
            with open(commit_filename) as commit_file:
                processed_commit_message = process_commit_message(commit_file.read())
            with open(commit_filename, 'w') as commit_file:
                commit_file.write(processed_commit_message)
        except ValueError as err:
            print_error(str(err))
            sys.exit(1)


if __name__ == '__main__':
    main()

    # Uncomment to run unit tests whenever script is changed
    # unittest.main()
